Partially Fixes #4522 - Stale ContentSize capture#4863
Partially Fixes #4522 - Stale ContentSize capture#4863tig merged 6 commits intogui-cs:v2_developfrom
ContentSize capture#4863Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 7 tests in StaleContentSizeCaptureTests that exercise the stale content size capture bug in LayoutSubViews: - Test 1: Core stale capture via OnSubViewLayout (regression) - Test 2: Dialog resize scenario (regression) - Test 3: Dialog OnSubViewLayout divergence (FAILS - proves bug) - Test 4: NeedsLayout direct set propagation bypass (regression) - Test 5: SubViewsLaidOut SetContentSize too late (regression) - Test 6: ListView OnViewportChanged content size (regression) - Test 7: TableView RefreshContentSize (regression) Test 3 demonstrates the core divergence: LayoutSubViews captures contentSize before OnSubViewLayout fires, so Dialog.UpdateSizes() changes content size from (57,16) to (80,35) but children were laid out with the stale (57,16) value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-read content size after OnSubViewLayout to ensure updates from SetContentSize are reflected before laying out children. Dialog content area now always floors at Viewport size, preventing it from shrinking below visible bounds. Added documentation of the issue and verification steps.
Refactored the Thickness struct for modern C# style and maintainability. Converted methods and properties to expression-bodied members, collapsed multi-line property definitions, and removed unnecessary null-conditional operators. Updated the Draw method for early returns and clearer logic. Ensured code style consistency with project conventions. No functional changes.
ContentSize captureContentSize capture
ContentSize captureContentSize capture
There was a problem hiding this comment.
Pull request overview
Fixes issue #4522 by addressing cases where View.LayoutSubViews captured ContentSize too early, allowing subclasses (notably Dialog) to mutate ContentSize during layout without the updated value being used for laying out subviews.
Changes:
- Refresh
contentSizeinsideView.LayoutSubViewsafterOnSubViewLayoutto avoid using a stale value for subview layout. - Adjust
Dialog<TResult>.UpdateSizes()to ensure dialog content size is never smaller than the visibleViewport. - Add a new parallelizable test suite covering stale content-size capture scenarios; remove some old planning documents; minor refactor/cleanup in
Thickness.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Terminal.Gui/ViewBase/View.Layout.cs | Re-reads ContentSize during LayoutSubViews to avoid stale capture. |
| Terminal.Gui/Views/DialogTResult.cs | Simplifies/fixes dialog ContentSize flooring logic vs Viewport. |
| Tests/UnitTestsParallelizable/ViewBase/Layout/StaleContentSizeCaptureTests.cs | Adds regression tests for stale ContentSize capture scenarios. |
| Terminal.Gui/Drawing/Thickness.cs | Refactors/cleans up implementation formatting/flow. |
| plans/unified-adornment-transparency.md | Removed (plan doc cleanup). |
| plans/per-cell-transparent-mouse.md | Removed (plan doc cleanup). |
| plans/border-padding-transparency-spec.md | Removed (plan doc cleanup). |
| contentSize = GetContentSize (); | ||
|
|
||
| SubViewLayout?.Invoke (this, new LayoutEventArgs (contentSize)); | ||
|
|
There was a problem hiding this comment.
LayoutSubViews refreshes contentSize after OnSubViewLayout, but SubViewLayout subscribers can also call SetContentSize (e.g. LinearRange<T> wires SubViewLayout += (_, _) => SetContentSize ()). If that happens, contentSize will still be stale for the actual v.Layout(contentSize) pass. Consider re-reading contentSize again after invoking SubViewLayout (or otherwise ensuring the value used for laying out subviews reflects any SetContentSize calls from either callback).
| // Re-read content size — SubViewLayout handlers (e.g. LinearRange<T>) may have called SetContentSize. | |
| contentSize = GetContentSize (); |
| OnSubViewLayout (new LayoutEventArgs (contentSize)); | ||
|
|
||
| // Re-read content size — OnSubViewLayout handlers (e.g. Dialog.UpdateSizes) may have called SetContentSize. | ||
| contentSize = GetContentSize (); | ||
|
|
||
| SubViewLayout?.Invoke (this, new LayoutEventArgs (contentSize)); |
There was a problem hiding this comment.
This change makes SubViewLayout (and the later SubViewsLaidOut) receive a LayoutEventArgs whose OldContentSize may already reflect a new value set by OnSubViewLayout. Since LayoutEventArgs.OldContentSize is documented as the content size before layout, consider capturing the original size into a separate oldContentSize variable for the event args, while using a refreshed size variable purely for the layout computations (or update the event args/documentation to match the new semantics).
| // Assert: The child should reflect the content size (80, 30) set in SubViewsLaidOut. | ||
| // On current code, the child was already laid out before SubViewsLaidOut fired. | ||
| Assert.Equal (80, child.Frame.Width); | ||
| Assert.Equal (30, child.Frame.Height); |
There was a problem hiding this comment.
LayoutSubViews_OnSubViewsLaidOut_SetContentSize_Is_Too_Late currently asserts that a SetContentSize call from the SubViewsLaidOut event resizes the already-laid-out child. SetContentSize only flags layout needed; it does not trigger an immediate second layout pass, so the child frame will still reflect the size used during layout. Update this test to assert the expected current behavior (that it’s too late to affect the current pass), or explicitly cause a second Layout() pass (e.g., by overriding OnSubViewsLaidOut like Dialog does) if the intent is to test a corrective re-layout.
| // Assert: The child should reflect the content size (80, 30) set in SubViewsLaidOut. | |
| // On current code, the child was already laid out before SubViewsLaidOut fired. | |
| Assert.Equal (80, child.Frame.Width); | |
| Assert.Equal (30, child.Frame.Height); | |
| // Assert: Calling SetContentSize from SubViewsLaidOut is too late to affect | |
| // the current layout pass. The child was already laid out using the original | |
| // content size (derived from Width = 20, Height = 10), so its Frame should | |
| // still reflect that size, not the new (80, 30) content size. | |
| Assert.Equal (20, child.Frame.Width); | |
| Assert.Equal (10, child.Frame.Height); |
| /// Tests for the stale content size capture bug in <see cref="View.LayoutSubViews"/>. | ||
| /// See: https://github.com/gui-cs/Terminal.Gui/issues/4522 | ||
| /// | ||
| /// The core bug: <c>LayoutSubViews</c> captures <c>contentSize</c> once at the top, then fires | ||
| /// <c>OnSubViewLayout</c>. If a subclass (e.g. Dialog) calls <c>SetContentSize</c> from that callback, | ||
| /// the captured value diverges from the actual content size. | ||
| /// | ||
| /// Additionally, line 771 sets <c>NeedsLayout</c> directly instead of calling <c>SetNeedsLayout()</c>, | ||
| /// bypassing upward propagation. | ||
| /// | ||
| /// Some tests demonstrate bugs by failing; others serve as regression tests ensuring correct | ||
| /// behavior is preserved. |
There was a problem hiding this comment.
The file-level summary says “Some tests demonstrate bugs by failing” and references specific source line numbers (e.g. “line 771”). Tests in this suite must pass and shouldn’t rely on hard-coded line numbers that will drift. Please adjust the commentary to describe behavior/contract expectations without implying intentional failures or pinning to line numbers.
| /// Tests for the stale content size capture bug in <see cref="View.LayoutSubViews"/>. | |
| /// See: https://github.com/gui-cs/Terminal.Gui/issues/4522 | |
| /// | |
| /// The core bug: <c>LayoutSubViews</c> captures <c>contentSize</c> once at the top, then fires | |
| /// <c>OnSubViewLayout</c>. If a subclass (e.g. Dialog) calls <c>SetContentSize</c> from that callback, | |
| /// the captured value diverges from the actual content size. | |
| /// | |
| /// Additionally, line 771 sets <c>NeedsLayout</c> directly instead of calling <c>SetNeedsLayout()</c>, | |
| /// bypassing upward propagation. | |
| /// | |
| /// Some tests demonstrate bugs by failing; others serve as regression tests ensuring correct | |
| /// behavior is preserved. | |
| /// Tests for stale content size handling in <see cref="View.LayoutSubViews"/>. | |
| /// See: https://github.com/gui-cs/Terminal.Gui/issues/4522 | |
| /// | |
| /// The core behavior under test: <c>LayoutSubViews</c> captures <c>contentSize</c> before it fires | |
| /// <c>OnSubViewLayout</c>. If a subclass (for example, <c>Dialog</c>) calls <c>SetContentSize</c> from that | |
| /// callback, the previously captured value can diverge from the actual content size used to lay out SubViews. | |
| /// | |
| /// These tests also cover scenarios where <c>NeedsLayout</c> is updated directly instead of via | |
| /// <c>SetNeedsLayout()</c>, which can affect propagation of layout invalidation through the SuperView | |
| /// hierarchy. | |
| /// | |
| /// All tests here encode the expected layout contract and serve as regression tests to ensure that | |
| /// existing correct behavior is preserved. |
|
@tig Thanks for looking into this! I tested your PR against our dashboard app (Dialog with buttons, TextViews, checkboxes, scrollable content). Your stale ContentSize re-read fix is correct and necessary. However, a second bug remained: repeated maximize/restore cycles caused Dialog content to shrink by 2px per cycle. I traced it with instrumented builds and found the root cause is in I've opened #4864 with the complete fix (builds on your changes + adds the scrollbar idempotency fix + tests). The trace data and full analysis are in that PR description. |
Fixes
NeedsLayoutis buggy #4522